<?php
/**
* class Auth
* <h2>用户认证模型</h2>
*
* <p>用户认证流程</p>
* <p>
* 本类的作用是完成用户的授权认证工作。登录分两种，一是使用账户和口令，
* 二是通过保留在用户cookie中的登录授权码做的记忆登录。
* 对于第一种情况，程序认为用户可信任，可以修改用户自己的敏感数据。
* 对于第二种情况，需要完成密码验证后才能修改敏感数据。
*
* 第一种登录流程如下：
* 用户提交账户和口令，服务器端程序做验证，如果是一个合法的用户，发送一个AUTHCODE(授权码)到客户端cookie，
* 同时会再发送一个TOKEN到客户端cookie中。
* AUTHCODE再用户的会话生命期内不变，除非碰到账号异常情况。TOKEN在会话生命期内每次请求都会更新。
* 登录后用户的AUTHCODE会被写入数据库中。
* 
* 该授权码作为用户后续访问的身份标识，表明用户已经登录，将展示用户个性化的信息。TOKEN用来检测
* 用户Cookie是否被盗。
* 当用户请求时，首先检测AUTHCODE，如果合法再检测TOKEN。当服务器上会话超时前，应该保存了一个TOKEN，
* 如果客户端发来TOKEN，则两个TOKEN比较，如果不一致时，则判定为“TOKEN不一致”，并在用户属性中进行计数，
* 然后更新用户的TOKEN。如果不一致超过阈值（如3次），则认为账户存在异常，可强制注销。
* 如果会话周期内，用户没有发来TOKEN，则认为用户篡改了Cookie或者客户端做了设置。直接注销用户。
*
* 如果是记忆登录，则根据保存在服务器上的AUTHCODE检查用户是否被授权过，如果是授权过的客户，更新客户端的TOKEN，
* 并记录该用户是记忆登录，在后续的操作中，访问敏感数据时候需要密码重新采用账户口令登录。
*
* </p>
*
*/
class Auth {
	
	/**
	 * <h2>日志记录对象</h2>
	 *
	 * @var Logger
	 */
	private $_logger;
	
	/**
	 * <h2>回话管理对象</h2>
	 *
	 * @var SessionManager
	 */
	private $_sessMgr = null;
	public function __construct() {
		Logger::configure ( ROOT_PATH . 'config/log_config.xml' );
		$this->_logger = Logger::getLogger ( __CLASS__ );
		$this->_sessMgr = SessionManager::getSingleton ();
	}
	
	/**
	 * <h2>判断用户是否已经授权</h2>
	 *
	 * <h3>TOKEN和AUTHCODE都沒有的情況</h3>
	 * <p>這種情況可能是用戶第一次訪問，或者系統做了清理工作</p>
	 * <p>處理這種情況直接不授權即可</p>
	 *
	 * <h3>什麽情況下會造成有AUTHCODE，沒有TOKEN</h3>
	 * <p>
	 * 1、網絡異常
	 * 2、客戶端人爲刪除了TOKEN（惡意調試）
	 * 3、用戶採用了記住登錄功能，在會話之間，TOKEN會由於過期而被客戶端瀏覽器刪除，AUTHCODE則由於過期時間很長，而保留。
	 * </p>
	 *
	 * <h3>什麽情況下回造成TOKEN不一致</h3>
	 *
	 * @return bool
	 */
	public function isAuthed() {
		if (! isset ( $_COOKIE ['AUTHCODE'] )) { // 客户端没有AUTHCODE,直接没有登录
			return false;
		}
		$userAgent = $_SERVER ['HTTP_USER_AGENT'];
		$httpAccept = $_SERVER ['HTTP_ACCEPT'];
		$authCode = sha1 ( $_COOKIE ['AUTHCODE'] . $userAgent . $httpAccept );
		/*
		 * 判断AUTHCODE是否合法 在会话没过期前，session中应该有authcode，直接比较。
		 * 如果会话中没有AUTHCODE，我们认为会话过期。检测数据库中是否有authcode，然后走第二種登錄流程
		 */
		if ($this->_sessMgr->getAttribute ( 'AUTHCODE' ) != null) { // 会话还没有过期
			if ($this->_sessMgr->getAttribute ( 'AUTHCODE' ) === $authCode) {
				// 檢查TOKEN
				if (! isset ( $_COOKIE ['TOKEN'] )) {
					$this->delAuth ();
					return false;
				}
				if ($_COOKIE ['TOKEN'] != $this->_sessMgr->getAttribute ( 'TOKEN' )) {
					// token如果不存在或者不對，則認爲客戶端登錄異常，但由於可能因爲網絡
					$exCnt = $this->_sessMgr->getAttribute ( 'TOKEN_EXCEPTION' );
					if (( int ) $exCnt >= 5) { // 連續5次TOKEN不正確
					                           //
						$this->delAuth ();
						// TODO log
						return false;
					}
					$this->_sessMgr->setAttribute ( 'TOKEN_EXCEPTION', ( int ) $exCnt + 1 );
					return true;
				} else {
					$this->_sessMgr->delAttribute ( 'TOKEN_EXCEPTION' );
					return true; // very good
				}
			} else {
				$this->delAuh ();
				return false; // AUTHCODE不一致
			}
		} else { // 會話過期
			$loginId = isset ( $_COOKIE ['LOGID'] ) ? ( int ) $_COOKIE ['LOGID'] : 0;
			if ($loginId != 0) {
				$query = 'SELECT loginId, ts_created, ts_lastaccess, user_id FROM logins WHERE id=' . $loginId . ' LIMIT 1';
				$conn = DBHelper::getDB ( false );
				$resultSet = $conn->select ( $query );
				if (count ( $resultSet )) {
					if ($authCode == $resultSet [0] ['loginId']) {
						if (( int ) $resultSet [0] ['ts_created'] + 60 * 60 * 24 * 7 < time ()) { // 一星期前記錄的登錄
							$this->delAuth ();
							return false;
						}
						$this->sendAuthCode ( $resultSet [0] ['user_id'] );
						$this->sendToken ();
						$this->_sessMgr->setAttribute ( 'REMBER_LOGIN', true );
						return true;
					} else {
						$this->delAuth ();
						return false;
					}
				} else {
					$this->delAuth ();
					return false;
				}
			} else {
				$this->delAuth ();
				return false;
			}
		}
	}
	
	/**
	 * <h2>设置授权码</h2>
	 * <p>用户登录成功后发送一个授权码到客户端cookie，
	 * 后边的请求只需要判断该授权码是否存在一个合法值来检验用户是否已经登录</p>
	 *
	 * @return void
	 */
	public function sendAuthCode($uid) {
		$this->_logger->debug ( 'sendAuthCode:uid=' . $uid );
		$tmpCode = GF::GenerateRandomString ( 40 );
		$userAgent = $_SERVER ['HTTP_USER_AGENT'];
		$httpAccept = $_SERVER ['HTTP_ACCEPT'];
		$authcode = sha1 ( $tmpCode );
		GF::setCookie ( 'AUTHCODE', $authcode, time () + 3600 );
		$srvAuthCode = sha1 ( $authcode . $userAgent . $httpAccept );
		$this->_sessMgr->setAttribute ( 'AUTHCODE', $srvAuthCode );
		$conn = DBHelper::getDB ( true, true );
		$query = 'INSERT INTO logins SET loginId=:loginId, ts_created=:ts_created, ts_lastaccess=:ts_lastaccess, user_id=:user_id';
		// @formatter:off
		$param['loginId'] = $srvAuthCode;
		$param['ts_created'] = time();
		$param['ts_lastaccess'] = time();
		$param['user_id'] = $uid;
		$this->_logger->debug($param);
		$map[] = array ('loginId','string');
		$map[] = array ('ts_created', 'int');
		$map[] = array ('ts_lastaccess', 'int');
		$map[] = array ('user_id', 'int');
		// @formatter:on
		$resultSet = $conn->insert ( $query, $param, $map );
		$this->_logger->debug ( 'LOGID' . $resultSet );
		if (! empty ( $resultSet )) {
			GF::setCookie ( 'LOGID', $resultSet );
		} else {
			$this->_logger->error ( 'LOG USER LOGIN FAILED' );
		}
	}
	
	/**
	 * <h2>更新授权码</h2>
	 * <p>主要是更新cookie的过期时间，不改变Authcode的值</p>
	 *
	 * @return void
	 */
	public function updateAuthCode() {
		if (isset ( $_COOKIE ['AUTHCODE'] )) {
			$authcode = $_COOKIE ['AUTHCODE'];
			GF::setCookie ( 'AUTHCODE', $authcode, time () + 3600 );
		}
	}
	
	/**
	 * <h2>发送TOKEN</h2>
	 */
	public function sendToken() {
		$token = GF::GenerateToken ( 40 );
		$hash_token = sha1 ( $token );
		GF::setCookie ( 'TOKEN', $hash_token );
		$this->_sessMgr->setAttribute ( 'TOKEN', $hash_token ); // 保存到会话中供比对
	}
	
	/**
	 * <h2>对提供的凭证进行验证</h2>
	 * <p>对用户提供的登录凭证进行授权认证，如果提供的凭证有效，返回真，并通过参数返回用户ID</p>
	 *
	 * @param CMAP $identity
	 *        	用户登录凭证
	 * @param int $uid
	 *        	返回参数，认证成功后，返回用户的ID
	 * @return scalar 方法的返回值，返回预定义的错误码
	 * @see errno.php
	 */
	public function authLogin(CMAP $identity, &$uid) {
		if ($identity == null) {
			return INVALID_IDENTITY;
		}
		// 当前只校验这两项
		$email = $identity->get ( 'email' );
		$password = $identity->get ( 'password' );
		// 取得用户salt
		$query = 'SELECT id, password, salt, ext_flags FROM users WHERE email=:email LIMIT 1';
		$param ['email'] = $email;
		// @formatter:off
		$map [] = array ('email','string');
		// @formatter:on
		$conn = DBHelper::getDB ( true, true );
		$resultSet = $conn->select ( $query, $param, $map );
		if (count ( $resultSet ) == 0) {
			return USER_NOT_EXISTS;
		}
		$saltVersion = $resultSet [0] ['ext_flags'] & USER_EFLAGS_SALT_MASK;
		// 判断用户凭证是否正确
		$saltedPass = GF::saltPassword ( $password, $resultSet [0] ['salt'], $saltVersion );
		if ($resultSet [0] ['password'] === $saltedPass) {
			$uid = $resultSet [0] ['id'];
			$this->_logger->debug ( 'uid=' . $uid );
			return STATUS_SUCCESS;
		} else {
			return STATUS_FAILED;
		}
	}
	
	/**
	 * <h2>刪除授權</h2>
	 */
	public function delAuth() {
		if (isset ( $_COOKIE ['AUTHCODE'] )) {
			GF::delCookie ( 'AUTHCODE' );
		}
		if (isset ( $_COOKIE ['TOKEN'] )) {
			GF::delCookie ( 'TOKEN' );
		}
	}
}
